﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection.Emit;
using System.Reflection;

namespace Kokomo.PeInspector.Reflection.Disassembly
{
	class OpcodeHandler
	{
		public readonly OpCode opcode;
		public readonly int? fixedInputCount;
		public readonly int? fixedOutputCount;
		public readonly Func<MethodBase, object, int> inputCountDelegate;
		public readonly Func<MethodBase, object, int> outputCountDelegate;
		public readonly Func<MethodBase, object, IEnumerable<MsilDependency>> dependencyDelegate;

		public OpcodeHandler(
			OpCode opcode,
			Action<MsilDecompilationContext, object> handlerDelegate,
			Func<MethodBase, object, int> inputCountDelegate = null,
			Func<MethodBase, object, int> outputCountDelegate = null,
			Func<MethodBase, object, IEnumerable<MsilDependency>> dependencyDelegate = null
			)
		{
			this.opcode = opcode;
			this.HandlerDelegate = handlerDelegate;
			this.dependencyDelegate = dependencyDelegate;

			if (inputCountDelegate == null)
			{
				this.fixedInputCount = DetermineFixedInputCount(opcode.StackBehaviourPop);
				if (!this.fixedInputCount.HasValue)
				{
					throw new ArgumentException(Messages.VariableInputOpcodesMissingDelegateMessage);
				}
			}
			else
			{
				this.inputCountDelegate = inputCountDelegate;
			}
			if (outputCountDelegate == null)
			{
				this.fixedOutputCount = DetermineFixedOutputCount(opcode.StackBehaviourPush);
				if (!this.fixedOutputCount.HasValue)
				{
					throw new ArgumentException(Messages.VariableOutputOpcodesMissingDelegateMessage);
				}
			}
			else
			{
				this.outputCountDelegate = outputCountDelegate;
			}
		}

		public IEnumerable<MsilDependency> GetDependencies(MethodBase method, object operand)
		{
			if (method == null) throw new ArgumentNullException("method");
			if (this.dependencyDelegate == null) return null;

			return this.dependencyDelegate(method, operand);
		}
		protected Action<MsilDecompilationContext, object> HandlerDelegate { get; private set; }
		public void Handle(
			MsilDecompilationContext context,
			object operand
			)
		{
			this.HandlerDelegate(context, operand);
		}

		private static int? DetermineFixedInputCount(StackBehaviour popBehavior)
		{
			int? inputCount = null;
			switch (popBehavior)
			{
				case StackBehaviour.Pop0:
					inputCount = 0;
					break;
				case StackBehaviour.Pop1:
				case StackBehaviour.Popi:
				case StackBehaviour.Popref:
					inputCount = 1;
					break;
				case StackBehaviour.Pop1_pop1:
				case StackBehaviour.Popi_pop1:
				case StackBehaviour.Popi_popi:
				case StackBehaviour.Popi_popi8:
				case StackBehaviour.Popi_popr4:
				case StackBehaviour.Popi_popr8:
				case StackBehaviour.Popref_pop1:
				case StackBehaviour.Popref_popi:
					inputCount = 2;
					break;
				case StackBehaviour.Popi_popi_popi:
				case StackBehaviour.Popref_popi_pop1:
				case StackBehaviour.Popref_popi_popi:
				case StackBehaviour.Popref_popi_popi8:
				case StackBehaviour.Popref_popi_popr4:
				case StackBehaviour.Popref_popi_popr8:
				case StackBehaviour.Popref_popi_popref:
					inputCount = 3;
					break;
			}
			return inputCount;
		}
		public int GetInputCount(MethodBase method, object operand)
		{
			int inputCount =
				(this.inputCountDelegate != null)
				? this.inputCountDelegate(method, operand)
				: this.fixedInputCount.Value
				;

			return inputCount;
		}
		private static int? DetermineFixedOutputCount(StackBehaviour pushBehavior)
		{
			int? outputCount = null;
			switch (pushBehavior)
			{
				case StackBehaviour.Push0:
					outputCount = 0;
					break;
				case StackBehaviour.Push1:
				case StackBehaviour.Pushi:
				case StackBehaviour.Pushi8:
				case StackBehaviour.Pushr4:
				case StackBehaviour.Pushr8:
				case StackBehaviour.Pushref:
					outputCount = 1;
					break;
				case StackBehaviour.Push1_push1:
					outputCount = 2;
					break;
			}

			return outputCount;
		}
		public int GetOutputCount(MethodBase method, object operand)
		{
			int outputCount =
				(this.inputCountDelegate != null)
				? this.inputCountDelegate(method, operand)
				: this.fixedInputCount.Value
				;

			return outputCount;
		}
	}
}
